import React from 'react';
import { renderToNodeStream } from 'react-dom/server';
import { Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';

import { RenderModel, RenderOption } from '../types';
import ServerSideRenderError from './errors/ssr-error';
import { HtmlStructure } from './html-struct';
import { addHeader } from './pipes';
import { addScripts } from './pipes/add-scripts';
import { addStyles } from './pipes/add-styles';

// create buff through controller result by renderToNodeStream
function createBuff(result: RenderModel) {
    return (onEnd: (buf: Buffer) => void, onError: (error: Error) => void) => {
        let buf = Buffer.from('<!DOCTYPE html>');

        const stream = renderToNodeStream(
            <HtmlStructure
                bodyElement={result?.renderOption?.rootElement}
                headOption={{
                    ...result?.headerOption,
                    metaList: result.metas,
                    injectedScripts: result.injectedScripts,
                    injectedStyles: result.injectedStyles,
                }}
                headerData={result?.headerOption}
                windowScripts={result.windowScripts}
                initData={result.controllerReply?.initData}
            />,
        );

        stream.on('data', (chunk) => {
            buf = Buffer.concat([buf, chunk]);
        });

        stream.on('end', () => {
            onEnd(buf);
        });

        stream.on('error', (err) => {
            // uncatch error
            onError(new ServerSideRenderError(err));
        });
    };
}

// pass react node stream to next through observable
function renderHtml(result: RenderModel): Observable<Buffer> {
    return new Observable<Buffer>((sub) => {
        createBuff(result)(
            (buf) => {
                sub.next(buf);
                sub.complete();
            },
            (err) => {
                sub.error(err);
            },
        );
    });
}

// render whole html node stream through pipes
function createHtml(controllerReply: Observable<any>, options: RenderOption) {
    return controllerReply.pipe(
        map((result) => ({ controllerReply: result, renderOption: options } as RenderModel)),
        map(addHeader),
        map(addStyles),
        map(addScripts),
        map(renderHtml),
        switchMap((_) => _), // convert multiple observable to one
    );
}

export { createHtml };
